/**!
 * 维护tantivy index的结构
 * 也是当前索引schema的定义:（docid,fname,body)
 * IndexWriter本身就是多线程的，不需要RwLock, 这里却仍然采用了tokio::sync::RwLock, 原因是可以避免声明writer的mut属性，带来编码的方便，代价却很小。 
 */

use tantivy::schema::*;
use tantivy::{doc, ReloadPolicy, Index};
use tantivy::schema::Field;
use tantivy::{IndexReader, IndexWriter };
use tantivy::directory::MmapDirectory;
use std::path::PathBuf;
use tantivy_jieba;
use std::fs;
use tokio::sync::RwLock;

use crate::*;

pub type Result<T> = anyhow::Result<T>;
/**
 * IndexWriter本身应该是多线程的，不需要RwLock，但这里通过牺牲RwLock性能，可以得到更好的编码体验(lengss).
 */
pub struct NodeIndex{
    index: Index,
    writer: RwLock <IndexWriter>,
    reader: IndexReader,
    docid: Field,    
    fname: Field,
    body:  Field  
}

/**
 * 对需要被检索的文本字段进行中文分词指定。  
 */
fn get_chs_textopts() -> TextOptions {
    let text_indexing = TextFieldIndexing::default()
            .set_tokenizer("jieba") // Set custom tokenizer
            .set_index_option(IndexRecordOption::WithFreqsAndPositions);
        TextOptions::default()
            .set_indexing_options(text_indexing)
            .set_stored()
}

impl NodeIndex {
    /**
     * 创建本地Node的tantivy index
     * 
     */
    pub fn new(
        mut base_path: PathBuf,
        index_name: &str,        
        writer_memory: usize       
    ) -> anyhow::Result<Self> {
        base_path.push(index_name);
        if !base_path.exists() {
            fs::create_dir(&base_path)?;
        }
        let dir = MmapDirectory::open(base_path)?;

        let mut schema_builder = Schema::builder();
        let docid = schema_builder.add_text_field("docid", STRING | STORED);
        let fname = schema_builder.add_text_field("fname", get_chs_textopts().clone());
        let body = schema_builder.add_text_field("body", get_chs_textopts().clone());
                
        let schema = schema_builder.build();
        let index = Index::open_or_create(dir, schema)?;
        let tokenizer = tantivy_jieba::JiebaTokenizer {};    
        
        index.tokenizers().register("jieba", tokenizer);        
        let index_writer = index.writer(writer_memory)?;
        // index_writer.set_merge_policy(merge_policy);
        let writer = RwLock::new(index_writer);
        let reader = index.reader_builder().reload_policy(ReloadPolicy::OnCommit).try_into()?;
        Ok(Self {
            index,
            reader,
            writer,
            docid,
            fname,
            body              
        })
    }

    /**
     * 获得当前索引定义的schema的3个Field对象(docid, fname, body)，用于索引或检索。
     * 
     */
    pub fn get_schema_tables(&self) -> (Field, Field, Field) {
        (self.docid.clone(), self.fname.clone(), self.body.clone() )
    }
    
    pub fn get_index(&self) -> &Index {
        &self.index
    }
    /**
     * 获得对当前索引的IndexReader引用。
     * 
     */
    pub fn get_reader(&self) -> &IndexReader {
        &self.reader
    }

    /**
     * 获得对当前索引的IndexWriter引用。 
     * 可以不用RwLock，IndexWriter本身就是多线程的。
     * 但如果不用RwLock，则需要返回&mut self.writer, 带来很多的编码改变。 
     */
    pub fn get_writer(&self) -> &RwLock<IndexWriter> {
       &self.writer
    } 

    /**
     * 索引指定的SearchDoc对象。
     * 
     */ 
    pub async fn add_document(&self, add_doc: &SearchDoc) -> Result<()> {        
        if self.doc_is_exist(add_doc.docid.as_str())?{
            return Ok(())
        }
        let mut index_writer = self.get_writer().write().await;
           let (docid, fname, body) = self.get_schema_tables();
            index_writer.add_document(doc!(
                docid => add_doc.docid.as_str(),                
                fname =>  add_doc.fname.as_str(),
                body =>  add_doc.content.as_str(),
                ))?;
            index_writer.commit()?;
        
        Ok(())
    }
    /**
     * 索引指定的文件，filename是路径文件，docid_code是sha256的hash code
     * 之所以需要fname（纯文件名），是为了避免解析filename的无谓消耗（外部rabbit中已经解析过）。
     */
    pub async fn add_file(&self, filename: &PathBuf, fname:&str, docid_code:&str) -> Result<()> {        
        if self.doc_is_exist(docid_code)?{
            return Ok(())
        }
        let mut index_writer = self.get_writer().write().await;
        let (docid_field, fname_field, body_field) = self.get_schema_tables();

        let mut content = String::new();
        // 以只读方式打开路径，返回 `io::Result<File>`
        File::open(&filename)?
            .read_to_string(&mut content)?;

        index_writer.add_document(doc!(
                docid_field => docid_code,                
                fname_field =>  fname,
                body_field =>  content.as_str(),
                ))?;
            index_writer.commit()?;
        
        Ok(())
    }

    /**
     * 检测索引中是否已经存在指定的docid。
     *  
     */
    pub fn doc_is_exist(&self, docid :&str) -> Result<bool> {
        let searcher = self.get_reader().searcher();
        let (docid_field, _fname, _body) = self.get_schema_tables();
        let docid_term = Term::from_field_text(docid_field, docid);    
        // This is the simplest query you can think of. It matches all of the documents containing a specific term.
        // The second argument is here to tell we don't care about decoding positions, or term frequencies.
        let term_query = TermQuery::new(docid_term, IndexRecordOption::Basic);
        let top_docs = searcher.search(&term_query, &TopDocs::with_limit(1))?;
        if let Some((_score, _doc_address)) = top_docs.first() {
            Ok(true)
        } else {
            // no doc matching this ID.
            Ok(false)
        }
    }
    /**
     * 从索引中读取指定docid的Document对象，或许应该返回SearchDoc，因为暂时没有使用场景，故先保持简单。
     * 
     */    
    pub fn read_doc_by_docid(&self, docid: &str) -> tantivy::Result<Option<Document>> {
        let searcher = self.get_reader().searcher();
        let (docid_field, _fname, _body) = self.get_schema_tables();
        let docid_term = Term::from_field_text(docid_field, docid);    
        // This is the simplest query you can think of. It matches all of the documents containing a specific term.
        // The second argument is here to tell we don't care about decoding positions, or term frequencies.
        let term_query = TermQuery::new(docid_term.clone(), IndexRecordOption::Basic);
        let top_docs = searcher.search(&term_query, &TopDocs::with_limit(1))?;
        if let Some((_score, doc_address)) = top_docs.first() {
            let doc = searcher.doc(*doc_address)?;
            Ok(Some(doc))
        } else {
            // no doc matching this ID.
            Ok(None)
        }
    }
    
}
